# Copyright (c) 2018 Red Hat, Inc. All rights reserved. This copyrighted
# material is made available to anyone wishing to use, modify, copy, or
# redistribute it subject to the terms and conditions of the GNU General Public
# License v.2 or later.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""The "test" command."""
from http import cookiejar
import json
import re

import yaml

from kpet import cmd_misc
from kpet import data
from kpet import misc
from kpet import patch
from kpet import run
from kpet import ssp


def build(cmds_parser, common_parser):
    """Build the argument parser for the test command."""
    _, action_subparser = cmd_misc.build(
        cmds_parser,
        common_parser,
        'test',
        help='Test',
    )
    list_parser = action_subparser.add_parser(
        "list",
        help='List tests',
        parents=[common_parser],
    )
    list_parser.add_argument(
        '--domains',
        metavar='REGEX',
        help='A regular expression matching names or slash-separated paths '
             'of domains to restrict tests to. Only tests targeting host '
             'types belonging to the matching domains (and their subdomains) '
             'will be listed. Not allowed, if the database has no domains '
             'defined. '
             'Run "kpet domain tree" to see all available domains.'
    )
    list_parser.add_argument(
        '-t',
        '--trees',
        metavar='REGEX',
        help='A regular expression matching the names of kernel trees, which '
             'listed tests should match. '
             'Run "kpet tree list" to see recognized trees. '
    )
    list_parser.add_argument(
        '-a',
        '--arches',
        metavar='REGEX',
        help='A regular expression matching the names of architectures, which '
             'listed tests should match. '
             'Run "kpet arch list" to see supported architectures. '
    )
    list_parser.add_argument(
        '-c',
        '--components',
        metavar='REGEX',
        help='A regular expression matching extra components included '
             'into the kernel build, which listed tests should match. '
             'Run "kpet component list" to see recognized components.'
    )
    list_parser.add_argument(
        '-s',
        '--sets',
        metavar='PATTERN',
        help='Test set pattern: regexes (fully) matching names of test sets, '
        'that listed tests should belong to, combined using &, |, !, and () '
        'operators, which can be escaped with \\. Run "kpet set list" to see '
        'available sets.'
    )
    list_parser.add_argument(
        '--tests',
        action='append',
        metavar='REGEX',
        help='A regular expression (fully) matching names of tests to list.'
    )
    list_parser.add_argument(
        '-o',
        '--output',
        default='text',
        choices=['yaml', 'json', 'text'],
        help='Change output format, "json" and "yaml" give more information.'
    )
    list_parser.add_argument(
        '--cookies',
        metavar='FILE',
        default=None,
        help='Cookies to send when downloading patches, Netscape-format file.'
    )
    list_parser.add_argument(
        '-f',
        '--file',
        action='append',
        metavar='PATH',
        default=[],
        dest='files',
        help='Specify a PATH of a changed file which listed tests should '
             'cover, if it is a source file. If none are specified, and no '
             'mailboxes are specified, no assumption is made whether, or '
             'which, files were changed, and all source file conditions of '
             'listed tests are ignored. '
    )
    list_parser.add_argument(
        '--high-cost',
        metavar='CONDITION',
        choices=[c.lower() for c in run.HighCostCondition.__members__],
        default=run.HighCostCondition.YES.name.lower(),
        help='Include high-cost tests according to the CONDITION - one of: '
             '"no"/"false", "targeted", "triggered", and "yes"/"true". '
             'Meaning, "never include", "include if targeted", '
             '"include if triggered", and "always include", respectively. '
             'Default is "yes".'
    )
    list_parser.add_argument(
        'mboxes',
        metavar='MBOX',
        nargs='*',
        default=[],
        help='URL/path of a patch mailbox changing the files which listed '
             'tests should cover, if they are sources. If none are '
             'specified, and no -f/--file option is specified, no '
             'assumption is made whether, or which, files were changed, '
             'and all source file conditions of listed tests are ignored.'
    )
    list_parser.add_argument(
        '--file-list',
        metavar='FILE',
        help='Similar to mboxes processing, but directly read the file '
             'names from a file, one per line.'
    )
    list_parser.add_argument(
        '--targeted',
        metavar=misc.ARGPARSE_TERNARY_METAVAR,
        type=misc.argparse_ternary,
        const=True,
        nargs='?',
        help='Only output tests targeting specified sources, '
             'if specified without value, or with value "true"/"yes". '
             'Only output tests not targeting specified sources, '
             'if specified with value "false"/"no". '
             'Output tests regardless of their targeted status, '
             'if not specified, or specified with value "ignore".'
    )
    list_parser.add_argument(
        '--triggered',
        metavar=misc.ARGPARSE_TERNARY_METAVAR,
        type=misc.argparse_ternary,
        const=True,
        nargs='?',
        help='Only output tests triggered by specified sources, '
             'if specified without value, or with value "true"/"yes". '
             'Only output tests not triggered by specified sources, '
             'if specified with value "false"/"no". '
             'Output tests regardless of their triggered status, '
             'if not specified, or specified with value "ignore".'
    )


# pylint: disable=too-many-branches
def main_create_scenario(args, database):
    """
    Create a scenario for specified test database and command-line arguments.

    Args:
        args:       Parsed command-line arguments.
        database:   The database to create a scenario for.

    Returns:
        The created scenario.
    """
    trees_target_sets = [None]
    arches_target_sets = [None]
    components_target_set = None
    match_sets = None

    cookies = cookiejar.MozillaCookieJar()
    if args.cookies:
        cookies.load(args.cookies)
    # Merge explicitly-specified changed files and files from patches
    files = set(args.files) | \
        patch.get_file_set_from_location_set(set(args.mboxes), cookies)
    if args.file_list:
        files |= misc.read_file_list(args.file_list)
    # If no files were specified
    if not args.mboxes and not files and not args.file_list:
        # Assume files are unknown
        files = None
    if database.all_domains is None:
        if args.domains is not None:
            raise Exception("Database has no domains specified, "
                            "but the --domains option is provided")
        domains = {None}
    else:
        domains = database.all_domains
        if args.domains is not None:
            domain_regex = re.compile(args.domains)
            domains = {
                path: domain
                for path, domain in domains.items()
                if domain_regex.fullmatch(path) or
                domain_regex.fullmatch(domain.name)
            }
            if not domains:
                raise Exception(f"Regular expression {args.domains!r} "
                                f"matches no domains")
    if args.trees is not None:
        trees_target_sets = [{x} for x in database.trees
                             if re.fullmatch(args.trees, x)]
        if database.trees and not trees_target_sets:
            raise Exception("No trees matched specified regular "
                            f"expression: {args.trees}")
    if args.arches is not None:
        arches_target_sets = [{x} for x in database.arches
                              if re.fullmatch(args.arches, x)]
        if database.arches and not arches_target_sets:
            raise Exception("No architectures matched specified regular "
                            f"expression: {args.arches}")
    if args.components is not None:
        components_target_set = set(x for x in database.components
                                    if re.fullmatch(args.components, x))
        if database.components and not components_target_set:
            raise Exception("No components matched specified regular "
                            f"expression: {args.components}")
    if args.sets is not None:
        try:
            match_sets = ssp.compile(args.sets, set(database.sets))
        except (ssp.Error) as exc:
            raise Exception(
                f"Failed parsing set pattern: {repr(args.sets)}"
            ) from exc

    scenario = run.Scenario(database)
    high_cost = run.HighCostCondition[args.high_cost.upper()]
    for trees_target_set in trees_target_sets:
        for arches_target_set in arches_target_sets:
            scenario.add_scene(
                domain_paths=list(domains),
                target=data.Target(arches=arches_target_set,
                                   trees=trees_target_set,
                                   components=components_target_set),
                files=files,
                high_cost=high_cost,
                match_sets=match_sets,
                test_regexes=args.tests
            )
    return scenario


def main_list(scenario, output_type, targeted, triggered):
    """
    Execute `test list`.

    Args:
        scenario:    A scenario to list tests from.
        output_type: Type of the printed output.
        targeted:    True, if only "targeted" tests should be output.
                     False, if no "targeted" tests should be output.
                     None, to disregard "targeted" status.
        triggered:   True, if only "triggered" tests should be output.
                     False, if no "triggered" tests should be output.
                     None, to disregard "triggered" status.
    """
    tests_targeted_sources = scenario.get_tests_targeted_sources()
    tests_triggered_sources = scenario.get_tests_triggered_sources()
    output_data = []
    for test in scenario.get_tests():
        triggered_sources = tests_triggered_sources[test]
        targeted_sources = tests_targeted_sources[test]
        if not (
               (triggered is None or
                triggered == bool(triggered_sources)) and
               (targeted is None or
                targeted == bool(targeted_sources))
           ):
            continue
        test_dict = {}
        test_dict["name"] = test.name
        test_dict["universal_id"] = test.universal_id
        if test.origin:
            test_dict["origin"] = test.origin
        if test.location:
            test_dict["location"] = test.location
        test_dict["maintainers"] = test.maintainers
        if targeted_sources is not None:
            test_dict["targeted_sources"] = list(targeted_sources)
        if triggered_sources is not None:
            test_dict["triggered_sources"] = list(triggered_sources)
        test_dict["host_types"] = list({
            host.type_name
            for scene in scenario.scenes
            for recipeset in scene.recipesets
            for host in recipeset
            for test_run in host.tests
            if test is test_run.data
        })
        test_dict["environment"] = test.environment.copy()
        output_data.append(test_dict)
    output_data = sorted(output_data, key=lambda i: i["name"])

    if output_type == "text":
        for test in output_data:
            print(test["name"])
    elif output_type == "yaml":
        print(yaml.dump(output_data, Dumper=misc.YamlDumper,
                        default_flow_style=False, sort_keys=False), end="")
    elif output_type == "json":
        print(json.dumps(output_data, indent=4))


def main(args):
    """Execute the `test` command."""
    if not data.Base.is_dir_valid(args.db):
        misc.raise_invalid_database(args.db)
    database = data.Base(args.db)

    if args.action == 'list':
        main_list(main_create_scenario(args, database),
                  args.output, args.targeted, args.triggered)
    else:
        misc.raise_action_not_found(args.action, args.command)
